/*
 * XP3 Archive:
 *
 * Starts with a "XP3\r\n \n\x1A\x8B\x67\x01" magic, followed by a 8 byte offset and a file version. If file version is zero, then
 * this offset is the table's offset, otherwise points to an information block with flag (1 byte), unused (8 bytes), real table
 * offset (8 bytes) triplet.
 *               a)                                 b)
 *   [magic] [offset] [version 0]         [magic] [offset] [version 1]
 *             \--> [table]                         \-->[flag 0x80] [unused] [offset]
 *                                                                             \--> [table]
 *
 * Then the table itself starts with a flag (1 byte) and a table size (8 bytes). If flag is zero, then that's it, however if flag
 * isn't zero, then this size is the compressed size and uncompressed size (another 8 bytes) follows. The table header is either
 * followed by the table data, or (if flag non-zero) a zlib compressed table data.
 *               a)                                 b)
 *   | table                      |     | table                                                               |
 *   |[flag 0] [size] [table data]|     |[flag 1] [compressed size] [uncompressed size] [zlib compressed data]|
 *                                                                                         \--> [table data]
 *
 * Table consists of magic (4 bytes), size (8 bytes) and data (size bytes) triplets. One of the triplets (with magic "File") is
 * nested and contains further triplets in its data, with the same format.
 *
 *   [magic "File"] [size] | data                                                        | [magic "File"] [size] [data] ...
 *                         |[magic "info"] [size] [data] [magic "segm"] [size] [data] ...|
 *
 * Here "info" magic contains encryption key (4 bytes), original file size (8 bytes), compressed file size (8 bytes), filename
 * length (2 bytes), followed by an UCS-2 filename. The "segm" block describes where the file contents are located in the archive
 * with multiple records in its data, so its size is always multiple of 28. Each record goes like compressed flag (4 bytes), file
 * offset (8 bytes), original segment size (8 bytes) and compressed segment size (8 bytes). File's content is made up from these
 * individually zlib compressed segments.
 *
 *   [magic "info"] [size] | data                                                                   |
 *                         |[comp flag] [total original size] [total comp size] [namelen] [filename]|
 *
 *   [magic "segm"] [size] | data, 28 byte records                                                                       |
 *                         |[comp flag] [offset] [orig size] [comp size] [comp flag] [offset] [orig size] [comp size] ...|
 *                                         \--> [compressed segment]                    \--> [compressed segment]
 *
 *   [magic "adlr"] [size 4] [data, Adler32 checksum, also encryption key]
 *
 * There are several other magics (both nested within "File" and at the same level as "File"), but unknown blocks can be skipped.
 * There could be "eliF" (or sometimes "hnfn") blocks at the same level as "File", with the same encryption key (that's what
 * connects them to their corresponding "File" block, 4 bytes), file name length (2 bytes) and UCS-2 file name. I guess this was
 * for version 0 maybe? Anyway, if "eliF" (or "hnfn") block exists then the file name in the "info" is not used.
 *
 *   [magic "eliF"/"hnfn"] [size] | data                                  | [magic "File"] [size] [data] ...
 *                                |[Adler32 checksum] [namelen] [filename]|
 */
#include "main.h"
#include "../zlib.h"

#define XP3MAGIC "XP3\r\n \n\x1A\x8B\x67\x01"

static uint64_t total, current, xp3total;
static uint8_t encmethod = 0, xormask[16] = { 0 };
static uint32_t xorhash = 0, xoradlr = 0;

/* file index */
typedef struct {
    uint64_t offs;
    uint64_t orig;
    uint64_t comp;
} seg_t;

typedef struct {
    char name[NAMELEN];
    uint64_t orig;
    uint64_t comp;
    uint32_t adlr, key;
    int numseg;
    seg_t *seg;
} index_t;
static index_t *idx = NULL;
static int numidx = 0;

/**
 * Given the detected xor mask, get encryption type and key
 */
static void detect_encryption(void)
{
    encmethod = 0;
    if(!xorhash) return;
    /* TODO: using these values figure out which encryption method and key to use */
    debug("detected xor mask %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X "
        "(when key hash %08X and Adler %08X)", xormask[0], xormask[1], xormask[2], xormask[3], xormask[4],
        xormask[5], xormask[6], xormask[7], xormask[8], xormask[9], xormask[10], xormask[11], xormask[12],
        xormask[13], xormask[14], xormask[15], xorhash, xoradlr);
}

/**
 * Decrypt a buffer
 */
static uint64_t decrypt_file(uint8_t *buf, uint64_t len, uint64_t offs)
{
    if(!encmethod || !buf || !len) return len;
    (void)offs;
    /* TODO: implement deobfuscation */
    switch(encmethod) {
        case 1:
        break;
        case 2:
        break;
    }
    return len;
}

/* helpers to getfileindex() */
static int addseg(index_t *idxrec, seg_t *seg)
{
    int i = idxrec->numseg++;
    if(!(idxrec->seg = (seg_t*)realloc(idxrec->seg, idxrec->numseg * sizeof(seg_t)))) {
        debug("unable to allocate memory, too many segments %u", idxrec->numseg);
        idxrec->numseg = 0;
    } else
        memcpy(&idxrec->seg[i], seg, sizeof(seg_t));
    return i;
}

static int addfileindex(index_t *idxrec)
{
    int i;

    for(i = 0; i < numidx; i++)
        if(idxrec->adlr == idx[i].adlr) {
            if(idxrec->seg) {
                if(idx[i].seg) free(idx[i].seg);
                idx[i].seg = idxrec->seg;
                idx[i].numseg = idxrec->numseg;
            }
            if(!idxrec->seg || !idx[i].name[0])
                strcpy(idx[i].name, idxrec->name);
            return i;
        }
    i = numidx++;
    if(!(idx = (index_t*)realloc(idx, numidx * sizeof(index_t)))) {
        debug("unable to allocate memory, too many table entries %u", numidx);
        numidx = 0; return 0;
    }
    memcpy(&idx[i], idxrec, sizeof(index_t));
    if(verbose > 1) debug("file in archive '%s' (%lu bytes)", idx[i].name, idx[i].orig);
    return i;
}

/**
 * Free file index
 */
static void freefileindex(void)
{
    int i;
    if(idx) {
        for(i = 0; i < numidx; i++)
            if(idx[i].seg) free(idx[i].seg);
        free(idx); idx = NULL;
    }
    numidx = 0;
}

/**
 * Get file index (parse XP3 archive)
 */
static int getfileindex(void *f, uint64_t size)
{
    index_t idxrec;
    uint8_t tmp[32], version, *table = NULL, *ptr, *ptr2, *end;
    uint64_t offs, comp, orig, len, len2;
    char *str;
    int i, n;

    freefileindex();
    if(!f) return 0;

    /* parse header */
    fileread(f, tmp, sizeof(tmp));
    if(memcmp(tmp, XP3MAGIC, 11)) return 0;
    memcpy(&offs, tmp + 11, sizeof(offs));
    version = tmp[19];
    debug("krkr version %u %s offs %lx", version, !version ? "table" : "info", offs);
    if(offs + 17 > size) return 0;

    fileseek(f, offs);
    fileread(f, tmp, 17);
    /* KrKrZ */
    if(version == 1) {
        memcpy(&offs, tmp + 9, sizeof(offs));
        debug("krkrz table offs %lx", offs);
        if(offs + 17 > size) return 0;
        fileseek(f, offs);
        fileread(f, tmp, 17);
    }
    memcpy(&comp, tmp + 1, sizeof(comp));
    memcpy(&orig, tmp + 9, sizeof(orig));

    /* table compressed flag */
    if(tmp[0] & 7) {
        debug("compressed table %lu bytes, uncompressed %lu bytes", comp, orig);
        if(!(table = (uint8_t*)malloc(orig))) { engine_features |= EF_NOMEM; debug("unable to allocate memory for table"); goto error; }
        memset(table, 0, orig);
        if(!(ptr = (uint8_t*)malloc(comp))) { engine_features |= EF_NOMEM; debug("unable to allocate memory for compressed table"); goto error; }
        fileread(f, ptr, comp);
        uncompress(table, &orig, ptr, comp);
        free(ptr);
        if(!orig) goto error;
    } else {
        orig = comp;
        debug("uncompressed table %lu bytes", orig);
        if(!(table = (uint8_t*)malloc(orig))) { engine_features |= EF_NOMEM; debug("unable to allocate memory for table"); goto error; }
        memset(table, 0, orig);
        fileseek(f, offs + 9);
        fileread(f, table, orig);
    }
    end = table + orig;

    for(ptr = table; ptr < end && *ptr; ptr += len) {
        memset(&idxrec, 0, sizeof(idxrec));
        memcpy(&len, ptr + 4, sizeof(len)); len += 12;
        if(!memcmp(ptr, "eliF", 4) || !memcmp(ptr, "hnfn", 4)) {
            memcpy(&idxrec.adlr, ptr + 12, sizeof(idxrec.adlr));
            for(str = idxrec.name, n = ptr[16] | (ptr[17] << 8), i = 0; i < n && (uintptr_t)str - (uintptr_t)idxrec.name < NAMELEN - 4; i++)
                str = toutf8(str, ptr[18 + i * 2] | (ptr[18 + i * 2] << 8));
            addfileindex(&idxrec);
        } else
        if(!memcmp(ptr, "File", 4)) {
            for(ptr2 = ptr + 12; ptr2 < ptr + len; ptr2 += len2) {
                memcpy(&len2, ptr2 + 4, sizeof(len2)); len2 += 12;
                if(!memcmp(ptr2, "info", 4)) {
                    memcpy(&idxrec.key,  ptr2 + 12, sizeof(idxrec.key));
                    memcpy(&idxrec.orig, ptr2 + 16, sizeof(idxrec.orig));
                    memcpy(&idxrec.comp, ptr2 + 24, sizeof(idxrec.comp));
                    for(str = idxrec.name, n = ptr2[32] | (ptr2[33] << 8), i = 0; i < n && (uintptr_t)str - (uintptr_t)idxrec.name < NAMELEN - 4; i++)
                        str = toutf8(str, ptr2[34 + i * 2] | (ptr2[35 + i * 2] << 8));
                } else
                if(!memcmp(ptr2, "segm", 4)) {
                    for(i = 0; (uint32_t)i + 12 < len2; i += 28) {
                        if(!(ptr2[12 + i] | (ptr2[13 + i] << 8)))
                            memset(ptr2 + 32 + i, 0, sizeof(uint64_t));
                        addseg(&idxrec, (seg_t*)(ptr2 + 16 + i));
                    }
                } else
                if(!memcmp(ptr2, "adlr", 4))
                    memcpy(&idxrec.adlr, ptr2 + 12, sizeof(idxrec.adlr));
                else if(memcmp(ptr2, "time", 4))
                    debug("unknown nested table entry in File entry %02%02%02%02 (%c%c%c%c)", ptr2[0], ptr2[1], ptr2[2], ptr2[3],
                        ptr2[0] >= ' ' && ptr2[0] < 0x7f ? ptr2[0] : '?', ptr2[1] >= ' ' && ptr2[1] < 0x7f ? ptr2[1] : '?',
                        ptr2[2] >= ' ' && ptr2[2] < 0x7f ? ptr2[2] : '?', ptr2[3] >= ' ' && ptr2[3] < 0x7f ? ptr2[3] : '?');
            }
            addfileindex(&idxrec);
        } else
            debug("unknown table entry %02%02%02%02 (%c%c%c%c)", ptr[0], ptr[1], ptr[2], ptr[3],
                ptr[0] >= ' ' && ptr[0] < 0x7f ? ptr[0] : '?', ptr[1] >= ' ' && ptr[1] < 0x7f ? ptr[1] : '?',
                ptr[2] >= ' ' && ptr[2] < 0x7f ? ptr[2] : '?', ptr[3] >= ' ' && ptr[3] < 0x7f ? ptr[3] : '?');
    }

error:
    if(table) free(table);
    if(idx) {
        for(i = 0; i < numidx; i++)
            if(!idx[i].name[0] || !idx[i].numseg || !idx[i].seg) {
                if(idx[i].seg) free(idx[i].seg);
                memcpy(&idx[i], &idx[i + 1], (numidx - i - 1) * sizeof(index_t));
                i--; numidx--;
            }
        if(!numidx && idx) { free(idx); idx = NULL; }
        else debug("table has %u entries", numidx);
    }
    return numidx;
}

/**
 * Extract one file from an XP3
 */
static void unpack_file(char *path, int save)
{
    uint8_t *buf, *comp, *tlg = NULL;
    uint32_t *pix;
    uint64_t size, tlgsize, blksize, usize, offs, len, subtotal = 0;
    void *f = NULL, *o = NULL;
    char tmp[PATH_MAX + FILENAME_MAX + 1], *fn, *s;
    int i, j, w, h, l;

    debug("unpacking '%s'", path);
    if(!path || !*path || !(f = fileopen(path, &size))) return;
    strcpy(tmp, path);
    fn = strrchr(tmp, '.');
    if(!fn) { fn = tmp + strlen(tmp); *fn++ = '~'; }
    *fn = 0; if(save) { filemkdir(tmp); } *fn++ = '/';
    if(getfileindex(f, size)) {
        for(i = 0; !quit && i < numidx; i++) {
            strcpy(fn, idx[i].name);
            if(save)
                for(s = fn; s < fn + 256 && *s; s++)
                    if(*s == '/') { *s = 0; filemkdir(tmp); *s = '/'; }
            if(!save || (o = fileopen(tmp, NULL))) {
                if(save) debug("putfile '%s' %lu bytes", tmp, idx[i].orig);
                if(idx[i].key && !encmethod) {
                    debug("warning: encrypted file but no suitable deobfuscation method found");
                    if(!save) continue;
                }
                for(offs = tlgsize = 0, j = 0; j < idx[i].numseg; j++) {
                    blksize = idx[i].seg[j].comp ? idx[i].seg[j].comp : idx[i].seg[j].orig;
                    subtotal += blksize;
                    status(save ? SAVING : LOADING, current + subtotal, total);
                    if(idx[i].seg[j].offs + blksize > size)
                        debug("invalid segment #%u offs %lx size %lu", j, idx[i].seg[j].offs, blksize);
                    else {
                        if(!(buf = (uint8_t*)malloc(idx[i].seg[j].orig))) {
                            engine_features |= EF_NOMEM;
                            debug("segment #%u size %lu unable to allocate memory", j, idx[i].seg[j].orig);
                            break;
                        }
                        if(idx[i].seg[j].comp) {
                            if((comp = (uint8_t*)malloc(idx[i].seg[j].comp))) {
                                memset(comp, 0, idx[i].seg[j].comp);
                                fileseek(f, idx[i].seg[j].offs);
                                fileread(f, comp, idx[i].seg[j].comp);
                                usize = idx[i].seg[j].orig;
                                uncompress(buf, &usize, comp, idx[i].seg[j].comp);
                                free(comp);
                                if(!usize) {
                                    debug("segment #%u offs %lx size %lu uncompression error", j,
                                        idx[i].seg[j].offs, idx[i].seg[j].comp);
                                    break;
                                }
                            } else {
                                free(buf);
                                engine_features |= EF_NOMEM;
                                debug("segment #%u size %lu unable to allocate memory", j, idx[i].seg[j].comp);
                                break;
                            }
                        } else {
                            usize = idx[i].seg[j].orig;
                            fileseek(f, idx[i].seg[j].offs);
                            fileread(f, buf, usize);
                        }
                        len = idx[i].key ? decrypt_file(buf, usize, offs) : usize;
                        offs += usize;
                        if(save) filewrite(o, buf, len);
                        if(!save || ((!j && (!memcmp(buf, "TLG", 3) || !memcmp(buf, "XXX", 3) || !memcmp(buf, "JKM", 3))) || tlgsize)) {
                            if((tlg = (uint8_t*)realloc(tlg, tlgsize + len))) { memcpy(tlg + tlgsize, buf, len); tlgsize += len; }
                            else { debug("segment #%u unable to allocate memory", j); tlgsize = 0; }
                        }
                        free(buf);
                    }
                }
                if(save) fileclose(o);
                if(tlg && tlgsize) {
                    if(save) {
                        if(engine_convert) {
                            if((pix = sprites_decode(tlg, tlgsize, &w, &h))) {
                                if((buf = stbi_write_png_to_mem((const unsigned char *)pix, w * 4, w, h, 4, &l))) {
                                    s = strrchr(tmp, '.'); if(!s) s = tmp + strlen(tmp);
                                    strcpy(s, ".png");
                                    putfile(tmp, buf, l, NULL, 0, NULL, 0);
                                    free(buf);
                                } else debug("unable to encode png");
                                free(pix);
                            } else debug("unable to decode tlg");
                        }
                        free(tlg);
                    } else {
                        if(!fileasset(-1, idx[i].name, tlg, tlgsize)) free(tlg);
                    }
                    tlg = NULL; tlgsize = 0;
                }
            } else debug("unable to write '%s'", tmp);
        }
        freefileindex();
    }
    fileclose(f);
}

/**
 * Detect if this is a KiriKiri game
 */
int krkr_detect(char *path, uint8_t *buf, uint64_t size)
{
    char tmp[PATH_MAX + FILENAME_MAX + 1];

    if(!path || !*path) return 0;
    if(buf && size > 32 && !memcmp(buf, XP3MAGIC, 11)) return 1;
    strcpy(tmp, path);
    strcat(tmp, "/krmovie.dll");
    if(fileexists(tmp)) return 1;
    strcat(tmp, "/data.xp3");
    return fileexists(tmp);
}

/**
 * Callback function for find() in krkr_load()
 */
int krkr_detectkeycb(char *path)
{
    uint8_t *buf, *comp, pnghdr[16] = { 0x89, 'P', 'N', 'G', 0x0D, 0x0A, 0x1A, 0x0A, 0, 0, 0, 0x0D, 'I', 'H', 'D', 'R' };
    uint64_t size;
    void *f = NULL;
    char *ext;
    int i;

    if((f = fileopen(path, &size))) {
        if(getfileindex(f, size)) {
            for(i = 0; !quit && i < numidx; i++)
                if((ext = strrchr(idx[i].name, '.')) && !memcmp(ext, ".png", 5) && idx[i].seg[0].orig > 16 && idx[i].key) {
                    if((buf = (uint8_t*)malloc(idx[i].seg[0].orig))) {
                        memset(buf, 0, idx[i].seg[0].orig);
                        if(idx[i].seg[0].comp) {
                            if((comp = (uint8_t*)malloc(idx[i].seg[0].comp))) {
                                memset(comp, 0, idx[i].seg[0].comp);
                                fileseek(f, idx[i].seg[0].offs);
                                fileread(f, comp, idx[i].seg[0].comp);
                                size = idx[i].seg[0].orig;
                                uncompress(buf, &size, comp, idx[i].seg[0].comp);
                                free(comp);
                            }
                        } else {
                            fileseek(f, idx[i].seg[0].offs);
                            fileread(f, buf, idx[i].seg[0].orig);
                        }
                        if(memcmp(pnghdr, buf, 16)) {
                            xorhash = idx[i].key;
                            xoradlr = idx[i].adlr;
                            for(i = 0; i < 16; i++) xormask[i] = buf[i] ^ pnghdr[i];
                        }
                        free(buf);
                    }
                    freefileindex();
                    fileclose(f);
                    return 0;
                }
            freefileindex();
        }
        fileclose(f);
    }
    return 1;
}

/**
 * Callback function for find() in krkr_load()
 */
int krkr_loadcb(char *path)
{
    uint8_t tmp[16], *buf;
    uint64_t size = 0;
    void *f = fileopen(path, &size);

    if(f) {
        fileread(f, tmp, sizeof(tmp));
        fileclose(f);
        if(size > 32 && !memcmp(tmp, XP3MAGIC, 11)) {
            xp3total += size;
            unpack_file(path, 0);
        } else
        if(size > 32 && fileknown(tmp) && (buf = getfile(path, &size))) {
            if(!fileasset(-1, path + pathlen, buf, size)) free(buf);
        }
    }
    current += size;
    status(LOADING, current, total);
    return 1;
}

/**
 * Load all assets from a KiriKiri game
 */
void krkr_load(char *path, int isdir)
{
    char tmp[PATH_MAX + FILENAME_MAX + 1];
    char *fn;

    if(!path || !*path) return;

    fn = strrchr(path, '/');
    if(!fn) fn = strrchr(path, '\\');
    if(fn && isdir) strncpy(engine_game, fn + 1, sizeof(engine_game) - 1);
    else strcpy(engine_game, "?");

    status(DETECTKEY, 0, 100);
    encmethod = 0; xorhash = xoradlr = 0; memset(xormask, 0, sizeof(xormask));
    find(path, 1, krkr_detectkeycb);
    detect_encryption();
    /* if(haskey) debug("encryption key..."); */

    status(LOADING, 0, 100);
    strcpy(tmp, path);
    pathlen = strlen(tmp);
    current = xp3total = 0; total = du(tmp);
    find(tmp, 1, krkr_loadcb);

    engine_features |= EF_DECRYPT | EF_CONVERT;
}

/**
 * Free any additional resources that krkr_load() allocated
 */
void krkr_unload(void)
{
    freefileindex();
}

/**
 * Callback function for find() in krkr_decrypt()
 */
int krkr_unpackcb(char *path)
{
    uint8_t tmp[16];
    uint64_t size = 0;
    void *f = fileopen(path, &size);

    if(f) {
        fileread(f, tmp, sizeof(tmp));
        fileclose(f);
        if(size > 32 && !memcmp(tmp, XP3MAGIC, 11)) {
            unpack_file(path, 1);
            current += size;
        }
    }
    status(SAVING, current, total);
    return 1;
}

/**
 * Decrypt and unpack all files in a KiriKiri game
 */
void krkr_decrypt(char *path, int isdir)
{
    char tmp[PATH_MAX + FILENAME_MAX + 1];

    (void)isdir;
    if(!path || !*path) return;

    status(SAVING, 0, 100);
    strcpy(tmp, path);
    current = 0;
    total = xp3total ? xp3total : du(tmp);
    find(tmp, 1, krkr_unpackcb);
}

/**
 * Draw map tiles to off-screen pixel buffer
 */
void krkr_drawmap(map_t *map)
{
    (void)map;
}

/**
 * Save translated text
 */
void krkr_translate(char *path)
{
    msgbox(0, UNIMPLEMENTED, path);
}

ENGINE_MODULE(krkr, "KiriKiri3 / KrkrZ / KAG XP3 Archive")
